package il.technion.cs236369.webserver.typeHandlers;

import il.technion.cs236369.webserver.*;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.util.Properties;

import org.apache.http.Header;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpDateGenerator;

/**
 * @author Eran Arbel
 * 
 * Request handler, specifically made for the processing of TSP pages
 * Given a tsp file to handle, it would:
 * 	1) read the file
 * 	2) save the html sections as strings
 * 	3) and the java segments (<? ?>) as methods
 * 	4) and write those into a java class meant to output an html file
 * 	5) compile the class
 * 	6) run it
 * 	7) return output
 * 		(the file must have out (PrintStream), session and params (for holding query parameters) variables
 *
 */
public class TSPEngine implements TypeHandler
{
	private static final String nl = System.getProperty("line.separator");
	private static int instance = 1;
	private final Properties props;
	private Session session; 

	// constructor with one Properties argument for passing of data
	public TSPEngine (Properties props)
	{
		this.props = props;
		// uses the number of the session passed through properties to pull the actual saved session
		this.session = SessionManager.getSession(Integer.valueOf(props.getProperty("session")).intValue());
		if (this.session == null)
			this.session = new Session();
	}
	
	/**
	 * handler function for the TSP Engine
	 * @param path - the relative path to the required file
	 * @return the body of the html to display
	 */
	@Override
	public HttpResponse handle (ExtendedHttpRequest request)
	{
		HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, 200, "OK");
		String date = new HttpDateGenerator().getCurrentDate();
		response.setHeader(HttpHeaders.DATE, date);
		response.setHeader(HttpHeaders.CONNECTION, HTTP.CONN_CLOSE);
		// Create a new instance name
		String instanceName = "TSP" + instance++;
		
		// build a java file
		String javaFile = CreateJavaFile(request.getPath(), instanceName);
		if (null == javaFile)
		{
			return get500Error();
		}
		
		// get a compiler and compile the java into a class file uniquely
		JavaCompile javaCompile = new JavaCompile();
		Class<?> clas;
		try
		{
			clas = javaCompile.compileAndLoad(javaFile, 
					"il.technion.cs236369.webserver.typeHandlers." + instanceName);
		} catch (ClassNotFoundException e)
		{
			System.err.println("Class Not Found Exception!");
			e.printStackTrace();
			return get500Error();
		} finally
		{
			delJava(javaFile);
		}
		
		// load the new java object and instantiate
		java.lang.reflect.Constructor<?> constructor = clas.getConstructors()[0];
		OutputStream out = new ByteArrayOutputStream();
		Object object;
		try
		{
			//object = constructor.newInstance(new Object[] {out, request.getParameters(),session});
			object = constructor.newInstance(new Object[] {request.getParameters(), out ,session});
		} catch (InstantiationException | IllegalAccessException
				| IllegalArgumentException | InvocationTargetException e)
		{
			System.err.println("Instantiation Exception!");
			e.printStackTrace();
			delJava(javaFile);
			delClass(instanceName);
			return get500Error();
		}
		
		// call upon the instance to create an html page
		try
		{
			clas.getDeclaredMethod("execute", new Class<?>[] {}).invoke(object);
		} catch (IllegalAccessException | IllegalArgumentException
				| InvocationTargetException | NoSuchMethodException
				| SecurityException e)
		{
			System.err.println("Invocation Exception!");
			e.printStackTrace();
			delJava(javaFile);
			delClass(instanceName);
			return get500Error();
		}
		
		// encode html page in the response
		StringEntity responseHtml;
		try
		{
			String XHTMLHeader = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\""+
					"\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n" +
					"<html xmlns=\"http://www.w3.org/1999/xhtml\">\n";
			String html = XHTMLHeader + out.toString();
			responseHtml = new StringEntity(html);
		} catch (UnsupportedEncodingException e)
		{
			System.err.println("Encoding Exception! HTML Encoding not Supported!");
			e.printStackTrace();
			delJava(javaFile);
			delClass(instanceName);
			return get500Error();
		}
		response.setEntity(responseHtml);
		response.setHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(responseHtml.getContentLength()));
		response.setHeader(HttpHeaders.CONTENT_TYPE, "text/html");
		return response;
	}

	/**
	 * Create a new java file
	 *  
	 * @param path - the path to the tsp file requested
	 * @param instanceName - the name of java file to create
	 * @return the path to the created java file or null if unsuccessful
	 */
	private String CreateJavaFile(String path, String instanceName)
	{
		try
		{
			// the full path of the java file
			String result = WebServer.baseDirPath
							+ "src\\il\\technion\\cs236369\\webserver\\typeHandlers\\"
							+ instanceName + ".java";
			// create required file writing stream set to page directory and the instance java file
			FileOutputStream outFile = new FileOutputStream(result);
			PrintStream filePrinter = new PrintStream(outFile);
			
			// Standard intro
			println(filePrinter, "package il.technion.cs236369.webserver.typeHandlers;");
			println(filePrinter, "import java.io.OutputStream;");
			println(filePrinter, "import java.io.PrintStream;");
			println(filePrinter, "import java.util.HashMap;");
			println(filePrinter, "public class " + instanceName);
			println(filePrinter, "{");
			println(filePrinter, "\tprivate PrintStream out;");
			println(filePrinter, "\tprivate Session session;");
			println(filePrinter, "\tHashMap<String, String> params;");
			println(filePrinter, "");
			println(filePrinter, "\tpublic " + instanceName + "(HashMap<String,String> params, OutputStream out, Session session)");
			println(filePrinter, "\t{");
			println(filePrinter, "\t\tthis.out = new PrintStream(out);");
			println(filePrinter, "\t\tthis.params = params;");
			println(filePrinter, "\t\tthis.session = session;");
			println(filePrinter, "\t}");
			println(filePrinter, "");
			println(filePrinter, "\tpublic void execute() throws Exception");
			println(filePrinter, "\t{");
			
			// read the file given with the path
			String content = getFile(path);
			String[] sections = null;
			// break up the file into html and java segments
			boolean lastInteration = false;
			content = content.replaceAll("&lt;", "<").replaceAll("&gt;", ">");
			while (content.length() > 0)
			{
				// break into 'before <?' and 'after <?'
				String[] text = null;
				if (content.contains("<?"))
				{
					sections = content.split("\\<\\?",2);
					text = sections[0].split("\r|\n|\r\n");
				}
				else{
					lastInteration = true;
					text = sections[1].split("\r|\n|\r\n");
				}
				// break text into lines
				
				for (String line : text)
				{
					if (line.contains("\\"))
						line.replaceAll("\\", "\\\\");	// correct slashes for in string slashes
					if (line.contains("\""))
						line.replaceAll("\"", "\\\"");  // correct quotes for in string quotes
					println(filePrinter,"\t\tout.print(\"" + line +"\");"); // make a printable line in the file
				}

				if (lastInteration)
					break;
				
				// check if there are still more code segments
				if (sections.length > 1)
				{
					// now split the remainder into 'before ?>' and 'after ?>'
					if (sections[1].contains("?>"))
						sections = sections[1].split("\\?\\>", 2);
					// break code into line
					
					String[] code = sections[0].split("\r|\n|\r\n");
					for (String line : code)
					{
						//System.out.println("\t\t" + line.trim());
						println(filePrinter,"\t\t" + line.trim());
					}
				}
				// revert for next iteration
				content = sections[1];	
			}
			
			//close method
			println(filePrinter, "\t}");
			//close class			
			println(filePrinter, "}");
			
			filePrinter.close();
			outFile.close();
			
			return result;
		} catch (IOException e)
		{
			System.err.println("IO Exception Error!");
			e.printStackTrace();
			return null;
		}
	}
	
	/**
	 * Used to finally clean up the java file after use
	 * @param path - the path to the file to be deleted
	 * @return true if and only if the file was successfully deleted
	 */
	private boolean delJava(String path)
	{
		File file = new File(path);
		return file.delete();
	}
	
	/**
	 * Used to finally clean up the class file after use
	 * @param path - the of the file to be deleted
	 * @return true if and only if the file was successfully deleted
	 */
	private boolean delClass(String name)
	{
		String sep = File.separator;
		File file = new File("bin" + sep + "il" + sep + "technion" + sep + "cs236369" + sep + "webserver"
							+ sep + "typeHandlers" + sep + name + ".class");
		return file.delete();
	}
	
	/**
	 * uses the out stream and the new line defined to print one line
	 * 
	 * @param out - the stream to print to
	 * @param in - the string to print
	 */
	void println(PrintStream out, String in)
	{
		out.print(in + nl);
	}

	/**
	 * reads the file given in the path and returns it as a String
	 * @param path - the path of the file to read
	 * @return the file as a String
	 * @throws IOException - in case of a read error
	 */
	String getFile(String path) throws IOException
	{
		File inFile = new File(path);
		BufferedReader reader = new BufferedReader(new FileReader(inFile));
		StringBuilder builder = new StringBuilder();
		String line;
		while ((line = reader.readLine()) != null)
		{
			builder.append(line + nl);
		}
		return builder.toString();
	}

	HttpResponse get500Error()
	{
		HttpResponse result = new BasicHttpResponse(HttpVersion.HTTP_1_1, 500, "Internal Server Error");
		result.setHeader(HttpHeaders.CONTENT_TYPE, "text/html");
		String date = new HttpDateGenerator().getCurrentDate();
		result.setHeader(HttpHeaders.DATE, date);
		result.setHeader(HttpHeaders.CONNECTION, HTTP.CONN_CLOSE);
		
		try {
			StringEntity errorpage = new StringEntity(
					"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\""+
					"\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n" +
					"<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"+
					"<html><head></head><body><p>Internal Server Error</p>"+
					"<p>The server encountered an internal error or misconfiguration and was unable to complete your request.</p>" +
					"<p>Please contact the server administrator, " +
					"webmaster@domain.com and inform them of the time the error occurred, " +
					"and anything you might have done that may have caused the error.</p>" +
					"<p>More information about this error may be available in the server error log.</p></body></html>");
			result.setEntity(errorpage);
			result.setHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(errorpage.getContentLength()));
			result.setHeader(HttpHeaders.CONTENT_TYPE, "text/html");
		} catch (UnsupportedEncodingException e)
		{
			e.printStackTrace();
		}
		return result;
	}
}
